use std::time;

use chrono::NaiveDateTime;
use sea_orm::{
    sea_query::OnConflict, ActiveValue, ColumnTrait, DatabaseConnection, EntityOrSelect,
    EntityTrait, QueryFilter, QuerySelect,
};

use aspect_ident::DeviceIdent;
use aspect_db_sql::{device, user, user_device};
use tracing::*;

use crate::{errors::*, password};

const USERNAME_LEN_LIMIT: usize = 30;

pub struct UserDatastore {
    dbc: DatabaseConnection,
}

impl UserDatastore {
    pub fn new(dbc: DatabaseConnection) -> Result<Self, Error> {
        Ok(Self { dbc })
    }

    async fn find_user_ok(&self, username: &str) -> Result<user::Model, Error> {
        user::Entity
            .select()
            .filter(user::Column::Username.eq(username))
            .one(&self.dbc)
            .await?
            .ok_or_else(|| Error::UnknownUser(username.to_owned()))
    }

    /// Returns the SERVER-LOCAL user ID.  This is taken directly from the
    /// database and should only be treated ephemerally.
    pub async fn find_user_id_ok(&self, username: &str) -> Result<i32, Error> {
        let user = self.find_user_ok(username).await?;
        Ok(user.id)
    }

    /// Creates a new user if they do not already exist.
    pub async fn create_user(&self, username: &str, password: &str) -> Result<(), Error> {
        if username.len() > USERNAME_LEN_LIMIT {
            return Err(Error::UsernameTooLong(username.to_owned()).into());
        }

        let create_time = get_now_ndt();
        let pw_digest = password::hash_password(password)?;

        let user_am = user::ActiveModel {
            username: ActiveValue::Set(username.to_owned()),
            active: ActiveValue::Set(true),
            created_at: ActiveValue::Set(Some(create_time)),
            password_digest: ActiveValue::Set(pw_digest),
            ..Default::default()
        };

        // Actually insert the user.
        let ts_start = time::Instant::now();
        let res = user::Entity::insert(user_am)
            .on_conflict(OnConflict::new().do_nothing().to_owned())
            .exec_without_returning(&self.dbc)
            .await?;
        let insert_dur = time::Instant::now() - ts_start;
        debug!(?insert_dur, "inserted user entry");

        if res > 0 {
            Ok(())
        } else {
            Err(Error::UserExists(username.to_owned()))
        }
    }

    /// Returns if the supplied password matches what's in the database.
    pub async fn check_user_password(&self, username: &str, password: &str) -> Result<bool, Error> {
        let m = self.find_user_ok(username).await?;
        Ok(password::check_password(password, &m.password_digest).is_ok())
    }

    /// Checks if the user owns some particular device.
    pub async fn check_user_owns_device(
        &self,
        username: &str,
        device: &DeviceIdent,
    ) -> Result<bool, Error> {
        let uid = self.find_user_id_ok(username).await?;
        let ud_m = user_device::Entity::find()
            .filter(
                user_device::Column::UserId
                    .eq(uid)
                    .and(user_device::Column::DeviceId.eq(device.to_vec())),
            )
            .limit(1)
            .one(&self.dbc)
            .await?;
        Ok(ud_m.is_some())
    }

    /// Gets the IDs of all of the user's devices.
    pub async fn get_user_devices(&self, username: &str) -> Result<Vec<DeviceIdent>, Error> {
        let uid = self.find_user_id_ok(username).await?;

        // We only care about the user's devices.  This is really inefficient
        // since we shouldn't have to actually fetch the data, we should be able
        // to use a sub query or something.
        //
        // See: https://www.sea-ql.org/SeaORM/docs/advanced-query/custom-select/
        let user_idents = user_device::Entity::find()
            .filter(user_device::Column::UserId.eq(uid))
            .all(&self.dbc)
            .await?;
        let devices =
            device::Entity::find()
                .filter(device::Column::IsMostRecentKnown.eq(true).and(
                    device::Column::DeviceId.is_in(user_idents.into_iter().map(|e| {
                        let v = Box::new(e.device_id);
                        sea_orm::Value::Bytes(Some(v))
                    })),
                ))
                .all(&self.dbc)
                .await?;

        let mut idents = Vec::new();
        for m in devices {
            let mut h = [0; 32];
            if m.device_id.len() != 32 {
                warn!(%username, "encountered invalid device ID in database");
                continue;
            }

            h.copy_from_slice(&m.device_id);
            let di = DeviceIdent::from_hash(h);
            idents.push(di);
        }

        Ok(idents)
    }
}

fn get_now_ndt() -> NaiveDateTime {
    chrono::Utc::now().naive_utc()
}
